昨天我們深入了解了斷言的各種用法,今天要學習 TDD 的精髓 —「紅綠重構循環」。
想像一下,你接到一個需求:「我們需要一個判斷質數的函數。」以前你可能直接開始寫程式,但現在我們要用 TDD 的方式:先寫測試(紅燈),再寫最簡實作(綠燈),最後改善代碼(重構)。
今天結束後,你將學會:
TDD 的核心是一個簡單而強大的三步循環:
🔴 紅燈(Red) ➜ 🟢 綠燈(Green) ➜ 🔵 重構(Refactor)
↑ ↓
← ← ← ← ← ← ← ← ← ← ← ← ← ← ←
紅燈階段的核心思想:先思考需求,再動手寫程式
建立 tests/Unit/Day03/MathUtilsTest.php
:
<?php
describe('Math Utilities', function () {
describe('isPrime function', function () {
it('identifies small prime numbers', function () {
// 還沒有 isPrime 函數,所以這個測試會失敗(紅燈)
expect(isPrime(2))->toBe(true);
expect(isPrime(3))->toBe(true);
expect(isPrime(5))->toBe(true);
});
it('identifies small composite numbers', function () {
expect(isPrime(4))->toBe(false);
expect(isPrime(6))->toBe(false);
expect(isPrime(9))->toBe(false);
});
});
});
執行測試:
php artisan test tests/Unit/Day03
預期結果:測試失敗,因為 isPrime
函數還不存在。這就是我們要的「紅燈」!
綠燈階段的核心思想:用最簡單的方法讓測試通過
建立 app/Math/MathUtils.php
:
<?php
namespace App\Math;
class MathUtils
{
public static function isPrime(int $n): bool
{
// 最簡單的實作:硬編碼我們測試的數字
if (in_array($n, [2, 3, 5])) {
return true;
}
if (in_array($n, [4, 6, 9])) {
return false;
}
return false; // 其他數字先回傳 false
}
}
建立測試輔助函數 tests/Helpers/functions.php
:
<?php
use App\Math\MathUtils;
if (!function_exists('isPrime')) {
function isPrime(int $n): bool {
return MathUtils::isPrime($n);
}
}
在 tests/TestCase.php
中引入這個檔案。
執行測試:
php artisan test tests/Unit/Day03
結果:測試通過!我們達到了綠燈階段。
重構階段的核心思想:在測試保護下,改善代碼品質
我們的硬編碼實作太醜了,讓我們重構:
<?php
namespace App\Math;
class MathUtils
{
public static function isPrime(int $n): bool
{
// 處理邊界情況
if ($n < 2) return false;
if ($n === 2) return true;
if ($n % 2 === 0) return false;
// 檢查奇數因子到 sqrt(n)
for ($i = 3; $i * $i <= $n; $i += 2) {
if ($n % $i === 0) return false;
}
return true;
}
}
執行測試確認重構成功:
php artisan test tests/Unit/Day03
測試仍然通過!重構成功。
讓我們做第二輪循環,增加邊界情況的測試:
it('handles edge cases', function () {
expect(isPrime(0))->toBe(false);
expect(isPrime(1))->toBe(false);
});
it('handles larger prime numbers', function () {
expect(isPrime(11))->toBe(true);
expect(isPrime(13))->toBe(true);
});
執行測試 - 全部通過!因為我們的重構實作已經正確處理了這些情況。
TDD 不只是技術,更是一種開發節奏:
看到這些「代碼異味」就該重構了:
重構前:
if ($age >= 18) { /* ... */ }
重構後:
const MIN_ADULT_AGE = 18;
if ($age >= self::MIN_ADULT_AGE) { /* ... */ }
重構前:
function calc($x) { return $x * 0.1; }
重構後:
function calculateTax(float $price): float {
const TAX_RATE = 0.1;
return $price * self::TAX_RATE;
}
Pest 讓 TDD 變得更簡潔、更直觀:
it('handles prime numbers', function () {
expect(isPrime(7))->toBe(true);
});
describe('Math Utilities', function () {
describe('isPrime function', function () {
// 測試邏輯分組清晰
});
});
與傳統 PHPUnit 相比,Pest 的語法更像在描述需求而非寫程式碼。這讓我們在 TDD 的紅燈階段更容易專注於「需求是什麼」,而不是「怎麼寫測試」。
完整 app/Math/MathUtils.php
:
<?php
namespace App\Math;
class MathUtils
{
public static function isPrime(int $n): bool
{
if ($n < 2) return false;
if ($n === 2) return true;
if ($n % 2 === 0) return false;
for ($i = 3; $i * $i <= $n; $i += 2) {
if ($n % $i === 0) return false;
}
return true;
}
}
完整 tests/Unit/Day03/MathUtilsTest.php
:
<?php
describe('Math Utilities', function () {
describe('isPrime function', function () {
it('identifies small prime numbers', function () {
expect(isPrime(2))->toBe(true);
expect(isPrime(3))->toBe(true);
expect(isPrime(5))->toBe(true);
});
it('identifies small composite numbers', function () {
expect(isPrime(4))->toBe(false);
expect(isPrime(6))->toBe(false);
expect(isPrime(9))->toBe(false);
});
it('handles edge cases', function () {
expect(isPrime(0))->toBe(false);
expect(isPrime(1))->toBe(false);
});
it('handles larger prime numbers', function () {
expect(isPrime(11))->toBe(true);
expect(isPrime(13))->toBe(true);
});
});
});
完整 tests/Helpers/functions.php
:
<?php
use App\Math\MathUtils;
if (!function_exists('isPrime')) {
function isPrime(int $n): bool {
return MathUtils::isPrime($n);
}
}
TDD 的紅綠重構循環看似簡單,但要真正掌握需要大量練習。它不只是技術方法,更是一種思維模式的轉變。
試著用 TDD 方式實作一個 isEven
函數:
記住 TDD 的節奏:紅燈 → 綠燈 → 重構,小步快跑!
明天我們將學習「測試結構和組織」,了解如何讓測試更清晰、更好維護。